feat: add reusable publish-preview workflow#223
Conversation
Extract the preview build preparation logic into a reusable composite GitHub Action so other repos can use the same logic. The jq filter is inlined and the source scope is parameterized.
|
@cryptodev-2s What are your thoughts on supporting the entire preview build workflow rather than just the "generate preview builds" step? Recently, we've added support for preview builds to other polyrepos, but currently we have to copy around the workflows. It would be nice to consolidate all of the steps into this repo. And we could make the action support both monorepos and polyrepos. What do you think? |
Yes make sense, let me do this. |
Add a `workflow_call` workflow that handles the full preview build flow (fork check, comment reaction, build, publish) for both monorepos and polyrepos. Consumer repos only need a ~13-line thin wrapper with the `issue_comment` trigger. Also add a centralized `generate-preview-build-message.sh` script that auto-detects mono vs polyrepo and produces the appropriate PR comment.
|
|
||
| - name: Prepare preview builds (monorepo) | ||
| if: ${{ inputs.is-monorepo }} | ||
| uses: MetaMask/github-tools/.github/actions/prepare-preview-builds@prepare-preview-builds-action |
There was a problem hiding this comment.
Hardcoded branch ref will break after PR merge
High Severity
The uses: reference at @prepare-preview-builds-action points to what appears to be this PR's development branch. Once the PR is merged and the branch is deleted, the monorepo build step will fail for every consumer of this reusable workflow. The publish job correctly resolves the ref dynamically from github.workflow_ref, but the build job hardcodes this ephemeral branch name instead.
Additional Locations (1)
| !./packages/*/node_modules/ | ||
| !./packages/*/src/ | ||
| !./packages/*/tests/ | ||
| !./packages/**/*.test.* |
There was a problem hiding this comment.
Monorepo artifact paths hardcode packages/ directory structure
Medium Severity
The monorepo upload glob ./packages/*/ and the sanitization find packages both hardcode the packages/ directory. However, prepare-preview-builds.sh dynamically discovers workspaces via yarn workspaces list, which works for any directory layout. Monorepos with workspaces outside packages/ (e.g., libs/, modules/) would have their manifests prepared but the built output wouldn't be uploaded or security-validated before publishing.
Additional Locations (1)
There was a problem hiding this comment.
All MetaMask monorepos use packages/, this matches the existing core workflow convention. If a repo ever uses a different layout, we can add a workspace-glob input then.
There was a problem hiding this comment.
We could generate this glob automatically by looping through the workspaces field in package.json. But yes I agree that probably don't need to worry about this for v1.
There was a problem hiding this comment.
What do you think about turning this into an action and then merging the prepare-preview-builds action into it?
(I have made suggestions below as if the workflow is not being converted, FYI)
| - name: Sanitize and validate artifact manifests | ||
| run: | | ||
| bad=0 | ||
| if [[ "${{ inputs.is-monorepo }}" == "true" ]]; then |
There was a problem hiding this comment.
Instead of injecting this should we set it in an environment variable and then read the variable?
There was a problem hiding this comment.
Done 788fc30, using IS_MONOREPO env var now instead of direct injection.
| # Build a JSON object of { name: version } for all non-root workspace packages. | ||
| package_json="$( | ||
| yarn workspaces list --no-private --json \ | ||
| | jq --slurp '[.[] | select(.location != ".")] | .[].location' --raw-output \ |
There was a problem hiding this comment.
Do we need to omit .? I thought --no-private should take care of that:
| | jq --slurp '[.[] | select(.location != ".")] | .[].location' --raw-output \ | |
| | jq --slurp --raw-output '.[].location' \ |
There was a problem hiding this comment.
Agreed 788fc30, removed the . exclusion — --no-private handles it.
| cat > preview-build-message.txt <<MSGEOF | ||
| Preview builds have been published.${docs_link} | ||
|
|
||
| \`\`\` | ||
| yarn add ${name}@${version} | ||
| \`\`\` | ||
| MSGEOF |
There was a problem hiding this comment.
Wondering if we should say:
| cat > preview-build-message.txt <<MSGEOF | |
| Preview builds have been published.${docs_link} | |
| \`\`\` | |
| yarn add ${name}@${version} | |
| \`\`\` | |
| MSGEOF | |
| cat <<-MSGEOF > preview-build-message.txt | |
| The following preview build has been published: | |
| \`\`\` | |
| ${name}@${version} | |
| \`\`\` | |
| MSGEOF | |
| if [[ -n $docs_link ]]; then | |
| cat <<-MSGEOF >> preview-build-message.txt | |
| ${docs_link} | |
| MSGEOF | |
| fi |
There was a problem hiding this comment.
Applied 788fc30, polyrepo now says "The following preview build has been published:" with name@version (no yarn add), and docs link appended separately.
| cat > preview-build-message.txt <<MSGEOF | ||
| Preview builds have been published.${docs_link} | ||
|
|
||
| <details> | ||
|
|
||
| <summary>Expand for full list of packages and versions.</summary> | ||
|
|
||
| \`\`\` | ||
| ${package_json} | ||
| \`\`\` | ||
|
|
||
| </details> | ||
| MSGEOF |
There was a problem hiding this comment.
Wondering if we should say:
| cat > preview-build-message.txt <<MSGEOF | |
| Preview builds have been published.${docs_link} | |
| <details> | |
| <summary>Expand for full list of packages and versions.</summary> | |
| \`\`\` | |
| ${package_json} | |
| \`\`\` | |
| </details> | |
| MSGEOF | |
| cat <<-MSGEOF > preview-build-message.txt | |
| Preview builds have been published. | |
| MSGEOF | |
| if [[ -n $docs_link ]]; then | |
| echo -n " ${docs_link}" >> preview-build-message.txt | |
| fi | |
| cat <<-MSGEOF >> preview-build-message.txt | |
| <details> | |
| <summary>Expand for full list of packages and versions.</summary> | |
| \`\`\` | |
| ${package_json} | |
| \`\`\` | |
| </details> | |
| MSGEOF |
There was a problem hiding this comment.
Applied 788fc30, docs link now appended separately after the header.
| while IFS=$'\t' read -r location name; do | ||
| echo "- $name" | ||
| prepare-preview-manifest "$location/package.json" | ||
| done < <(yarn workspaces list --no-private --json | jq --slurp --raw-output 'map(select(.location != ".")) | map([.location, .name]) | map(@tsv) | .[]') |
There was a problem hiding this comment.
I know that the script in core does this, but now that I look at this, do we need to exclude . here either? --no-private should take care of this as well.
| done < <(yarn workspaces list --no-private --json | jq --slurp --raw-output 'map(select(.location != ".")) | map([.location, .name]) | map(@tsv) | .[]') | |
| done < <(yarn workspaces list --no-private --json | jq --slurp --raw-output 'map([.location, .name]) | map(@tsv) | .[]') |
There was a problem hiding this comment.
Agreed, removed the . exclusion.
| # Build a JSON object of { name: version } for all non-root workspace packages. | ||
| package_json="$( | ||
| yarn workspaces list --no-private --json \ | ||
| | jq --slurp '[.[] | select(.location != ".")] | .[].location' --raw-output \ | ||
| | while read -r location; do | ||
| jq '{ (.name): .version }' "$location/package.json" | ||
| done \ | ||
| | jq --slurp 'add' | ||
| )" |
There was a problem hiding this comment.
A few things:
-
Instead of building an object here what do you think about listing out the names and versions? The current way that we output the list of versions makes it difficult to update a project's
package.jsonmanually. -
Do we need to omit
.?I thought--no-privateshould take care of that. -
We can use
xargsinstead of a loop. There is a script incorethat does something very similar: https://github.com/MetaMask/core/blob/416032dbbed5c2bf1340572964a09c91d6e06dc7/scripts/list-workspace-versions.sh
| # Build a JSON object of { name: version } for all non-root workspace packages. | |
| package_json="$( | |
| yarn workspaces list --no-private --json \ | |
| | jq --slurp '[.[] | select(.location != ".")] | .[].location' --raw-output \ | |
| | while read -r location; do | |
| jq '{ (.name): .version }' "$location/package.json" | |
| done \ | |
| | jq --slurp 'add' | |
| )" | |
| # Build a list of "<name>@<version>" for all non-root workspace packages. | |
| packages="$( | |
| yarn workspaces list --no-private --json | \ | |
| jq --raw-output '.location' | \ | |
| xargs -I{} cat '{}/package.json' | \ | |
| jq --raw-output '"\(.name)@\(.version)"' | |
| )" |
There was a problem hiding this comment.
Done 788fc30, now outputs name@version per line using xargs + jq, matching the pattern from core's list-workspace-versions.sh.
| - name: Generate preview build message | ||
| run: | | ||
| args=() | ||
| if [[ "${{ inputs.is-monorepo }}" == "true" ]]; then |
There was a problem hiding this comment.
Should we use environment variables instead of reading inputs directly?
There was a problem hiding this comment.
Done 788fc30, using IS_MONOREPO and DOCS_URL env vars.
| !./packages/*/node_modules/ | ||
| !./packages/*/src/ | ||
| !./packages/*/tests/ | ||
| !./packages/**/*.test.* |
There was a problem hiding this comment.
We could generate this glob automatically by looping through the workspaces field in package.json. But yes I agree that probably don't need to worry about this for v1.
| with: | ||
| name: preview-build-artifacts | ||
|
|
||
| - name: Resolve GitHub tools ref |
There was a problem hiding this comment.
Is this step necessary? I see that for instance check-changelog reads github.action_ref.
There was a problem hiding this comment.
github.action_ref is only available in composite actions, not reusable workflows. For workflow_call, we need to parse github.workflow_ref to extract the ref.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
| if [[ "$IS_MONOREPO" == "true" ]]; then | ||
| mapfile -t manifests < <(find packages -name package.json -not -path '*/node_modules/*') | ||
| else | ||
| manifests=(package.json) |
There was a problem hiding this comment.
Monorepo sanitization skips root package.json manifest
Medium Severity
In monorepo mode, the sanitization step only scans packages/ for package.json files but the root package.json is also included in the uploaded artifacts (line 142). A malicious PR could inject lifecycle scripts (e.g., postinstall) into the root package.json, which would execute during the yarn install --no-immutable step in the publish job. While the NPM token isn't in the environment at that point, the GITHUB_TOKEN with pull-requests: write permission is accessible to the running process.


Add a reusable
workflow_callworkflow so consumer repos can replace their copy-pastedpublish-preview.ymlwith a thin wrapper.New files
.github/workflows/publish-preview.yml— Reusable workflow with 4 jobs: fork check, comment reaction, build (no token), publish (with token). Supports both monorepos and polyrepos viais-monorepoinput..github/scripts/generate-preview-build-message.sh— Generates the PR comment (collapsible package list for monorepos, single install command for polyrepos)..github/actions/prepare-preview-builds/— Composite action for monorepo manifest preparation (from prior commit).Consumer example
Polyrepo working example cryptodev-2s/core#3
Monorepo working example cryptodev-2s/create-release-branch#3
Note
Medium Risk
Changes CI publishing flow and handles an NPM auth token; while it adds manifest sanitization and fork gating, misconfiguration could still affect publish safety or reliability.
Overview
Adds a reusable
workflow_callworkflow (.github/workflows/publish-preview.yml) that builds PR code, uploads artifacts, then publishes preview packages to NPM in a separate job gated by fork checks and optional GitHub environments.Introduces a composite action plus script (
.github/actions/prepare-preview-buildsand.github/scripts/prepare-preview-builds.sh) to rewrite package names/versions for preview scopes, add workspaceresolutionsto keep local linking working, and reinstall deps.Hardens the publish step by sanitizing artifact
package.jsonfiles (stripping pack/publish lifecycle scripts and blocking non-npmjs registries) and posts a PR comment listing the published preview versions (with monorepo/polyrepo support and adry-runoption).Written by Cursor Bugbot for commit 9db8b7e. This will update automatically on new commits. Configure here.